import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.net.*;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.io.Serializable;

class  DataPacket implements Serializable {

	private static final long serialVersionUID = 1L;
	public byte[] header = new byte[256];
	public byte[] data = new byte[256];
	
	DataPacket(){}
	
	DataPacket(byte[] h,byte[] d)
	{
		header = h;
		data = d;
	}
}

class Counter
{
      private int count;
      
      Counter(int c)
      {
    	  count = c;
      }
      
      public synchronized void increment() {
          int n = count;
          count = n+1;
      }
      
      public synchronized int getCount() {
    	  return count;
      }
      
      public synchronized void setCount(int c) {
    	  count = c;
      }
      
      public synchronized void decrement() {
    	  int n = count;
    	  count = n-1;
      }
}


class ClientSocket {

	private static final int q_size = 2;
	private static final int port = 1234;

	private BlockingQueue<DatagramPacket> in;
	private BlockingQueue<DatagramPacket> out;

	private DatagramSocket clientSocket;
	private InetAddress IPAddress;
	private byte[] sendData;
	private byte[] receiveData;
	private DatagramPacket receivePacket, sendPacket;
	private Counter count;
	private Thread imht, omht;
	private boolean close;
	private boolean accept;

	
	
	private class IMHT extends Thread {
		IMHT() {
		}

		public synchronized void run() {
			String data;
			while(true) {
				try{
				receiveData = new byte[1024];
				receivePacket = new DatagramPacket(receiveData, receiveData.length);
				clientSocket.receive(receivePacket);
				
				DataPacket dp = toDataPacket(receivePacket.getData());
				data = new String(dp.header);
				data = data.trim();
				if(data.equals("accept"))
				{
					accept = true;
				}
				else if(data.equals("ack"))
				{
					count.increment();
				}
				else if(data.equals("end"))
				{
					break;
				}
				else
				{
					in.put(receivePacket);
					System.out.println("Message Received");
					System.out.println("IMHT to IN");
					System.out.println("In Queue Capacity: " + in.remainingCapacity());
				}
				}catch(Exception e){}
			}
		}
	}

	private class OMHT extends Thread {
		OMHT() {
		}

		public synchronized void run() {
			
			while(true) {
				try{
					if(close && out.isEmpty())
						break;
					if((count.getCount() > 0) && (!out.isEmpty())) {
						clientSocket.send(out.take());
						count.decrement();
						System.out.println("Message Sent");
						System.out.println("OMHT to Server");
						System.out.println("How many packet can be sent to server: " + count.getCount());
					}
				}catch(Exception e){}
			}
		}
	}
	
	public byte[] toByteArray (DataPacket packet)
	{
	  byte[] bytes = new byte[1024];
	  ByteArrayOutputStream bos = new ByteArrayOutputStream();
	  try {
	    ObjectOutputStream oos = new ObjectOutputStream(bos); 
	    oos.writeObject(packet);
	    oos.flush(); 
	    oos.close(); 
	    bos.close();
	    bytes = bos.toByteArray ();
	  }
	  catch (Exception ex) {}
	  return bytes;
	}
	
	public DataPacket toDataPacket (byte[] bytes)
	{
	  DataPacket dp = new DataPacket();
	  try {
	    ByteArrayInputStream bis = new ByteArrayInputStream (bytes);
	    ObjectInputStream ois = new ObjectInputStream (bis);
	    dp = (DataPacket)ois.readObject();
	  }
	  catch (Exception ex) {}
	  return dp;
	}
	
	ClientSocket() throws Exception {
		clientSocket = new DatagramSocket();
		in = new ArrayBlockingQueue<DatagramPacket>(q_size);
		out = new ArrayBlockingQueue<DatagramPacket>(q_size);
		IPAddress = InetAddress.getByName("localhost");
		count = new Counter(2);
		Counter acceptCount = new Counter(1);
		close = false;
		accept = false;

		//send request to server
		String request = new String("request");
		DataPacket dp = new DataPacket(request.getBytes(),null);
		sendData = new byte[1024];
		sendData = toByteArray(dp);
		sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
		try{
			clientSocket.send(sendPacket);

			imht = new IMHT();
			omht = new OMHT();

			imht.start();
			omht.start();
		}catch (Exception e){}

		while((!accept) && (acceptCount.getCount() < 11)){
			clientSocket.send(sendPacket);
			wait(1000);
			acceptCount.increment();
		}
		
		if(!accept)
			System.exit(0);
	}
	

	public static void wait (int n){
	    long t0,t1;
	    t0=System.currentTimeMillis();
	    do{
	        t1=System.currentTimeMillis();
	    }
	    while (t1-t0<1000);
	}


	void send(byte[] data) throws Exception {
		String packet = new String("data");
		DataPacket dp = new DataPacket(packet.getBytes(),data);
		sendData = new byte[1024];
		sendData = toByteArray(dp);
		out.put(new DatagramPacket(sendData, sendData.length,IPAddress, port));
	}

	byte [] receive() throws Exception {
		DatagramPacket inPacket;
		String ack = new String("ack");
		DataPacket ackPacket = new DataPacket(ack.getBytes(),null);
		DataPacket dp = new DataPacket();
		try{
			inPacket = in.take();
			dp = toDataPacket(inPacket.getData());
			sendData = new byte[1024];
			sendData = toByteArray(ackPacket);
			sendPacket = new DatagramPacket(sendData,sendData.length,IPAddress,port);
			clientSocket.send(sendPacket);
		}catch (Exception e){}
		
		return dp.data;
	}

	void close() throws Exception {
		String end = new String("end");
		DataPacket endPacket = new DataPacket(end.getBytes(),null);
		sendData = new byte[1024];
		sendData = toByteArray(endPacket);
		sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
		try{
			clientSocket.send(sendPacket);
			close = true;
		}catch(Exception e){}
		System.out.println("Connection Closed");
	}
}



class Data {
	public InetAddress IP;
	public int port;
	public ConnectionSocket socket;

	Data(InetAddress IP, int port, ConnectionSocket socket)
	{
		this.IP = IP;
		this.port = port;
		this.socket = socket;
	}	
}

class ConnectionSocket {
	private static final int q_size = 2;
	private static final int connection_size = 2;

	private BlockingQueue<DatagramPacket> in;
	private BlockingQueue<DatagramPacket> out;
	
	public BlockingQueue<DatagramPacket> cimhtIn;
	public BlockingQueue<DatagramPacket> cimhtOut;

	private DatagramSocket connectionSocket;
	private Data[] connectionTable;
	private Counter connSocketCount;
	private InetAddress IPAddress;
	private int port;
	private Counter count;
	private Thread cimht, comht;
	private boolean close_server, close_client;

	private class CIMHT extends Thread {
		CIMHT() {
		}

		public synchronized void run() {
			String data;
			while(true) {
				try{
					byte[] receiveData = new byte[1024];
					DatagramPacket receivePacket = new DatagramPacket(receiveData,receiveData.length);
					receivePacket = cimhtIn.take();
					
					DataPacket recPacket = toDataPacket(receivePacket.getData());
					data = new String(recPacket.header);
					data = data.trim();
					if(data.equals("ack"))
					{
						count.increment();
					}
					else if(data.equals("end"))
					{
						close_client = true;
						break;
					}
					else
					{
						in.put(receivePacket);
						System.out.println("Message Received");
						System.out.println("CIMHT to IN");
						System.out.println("In Queue Capacity: " + in.remainingCapacity());
					}
				}catch(Exception e){}
			}
		}
	}

	private class COMHT extends Thread {
		COMHT() {
		}

		public synchronized void run() {
			
			while(true) {
				try{
					if(close_server && out.isEmpty())
						break;
					if((count.getCount() > 0) && (!out.isEmpty())) {
						cimhtOut.put(out.take());
						count.decrement();
						System.out.println("COMHT to OMHT");
						System.out.println("How many packet can be sent to client: " + count.getCount());
					}
				}catch(Exception e){}
			}
		}
	}
	
	public byte[] toByteArray (DataPacket packet)
	{
	  byte[] bytes = new byte[1024];
	  ByteArrayOutputStream bos = new ByteArrayOutputStream();
	  try {
	    ObjectOutputStream oos = new ObjectOutputStream(bos); 
	    oos.writeObject(packet);
	    oos.flush(); 
	    oos.close(); 
	    bos.close();
	    bytes = bos.toByteArray ();
	  }
	  catch (IOException ex) {}
	  
	  return bytes;
	}
	
	public DataPacket toDataPacket (byte[] bytes)
	{
	  DataPacket dp = new DataPacket();
	  try {
	    ByteArrayInputStream bis = new ByteArrayInputStream (bytes);
	    ObjectInputStream ois = new ObjectInputStream (bis);
	    dp = (DataPacket)ois.readObject();
	  }
	  catch (IOException ex) {}
	  catch (ClassNotFoundException ex) {}
	  
	  return dp;
	}
						
	ConnectionSocket(BlockingQueue<DatagramPacket> outgoing, Data[] ct, Counter c, int p, InetAddress ip) throws Exception {
		connectionSocket = new DatagramSocket();
		in = new ArrayBlockingQueue<DatagramPacket>(q_size);
		out = new ArrayBlockingQueue<DatagramPacket>(q_size);
		cimhtIn = new ArrayBlockingQueue<DatagramPacket>(2*connection_size);
		cimhtOut = outgoing;
		connectionTable = ct;
		connSocketCount = c;
		port = p;
		IPAddress = ip;
		count = new Counter(2);
		close_server = false;
		close_client = false;

		try{
			cimht = new CIMHT();
			comht = new COMHT();

			cimht.start();
			comht.start();
		}catch (Exception e){}
	}

	void send(byte[] data) throws Exception {
		byte[] sendData = new byte[1024];
		String header = new String("data");
		DataPacket dp = new DataPacket(header.getBytes(), data);
		sendData = toByteArray(dp);
		out.put(new DatagramPacket(sendData, sendData.length,IPAddress, port));
	}

	byte [] receive() throws Exception {
		DatagramPacket inPacket;
		String ack = new String("ack");
		DataPacket ackPacket = new DataPacket(ack.getBytes(), null);
		DataPacket receivePacket = new DataPacket();
		
		try{
			inPacket = in.take();
			receivePacket = toDataPacket(inPacket.getData());
			byte[] sendData = new byte[1024];
			sendData = toByteArray(ackPacket);
			DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,IPAddress,port);
			connectionSocket.send(sendPacket);
		}catch (Exception e){}
		
		return receivePacket.data;
	}

	void close() throws Exception {
		String end = new String("end");
		DataPacket endPacket = new DataPacket(end.getBytes(), null);
		byte[] sendData = new byte[1024];
		sendData = toByteArray(endPacket);
		DatagramPacket sendPacket = new DatagramPacket(sendData, sendData.length, IPAddress, port);
		try{
			while(!close_client){}
			cimhtOut.put(sendPacket);
			connSocketCount.decrement();
			int i;
			for(i = 0; i < connection_size; i++) {
				if(connectionTable[i] != null)
				{
					if(connectionTable[i].port == port && connectionTable[i].IP.equals(IPAddress))
					{
						connectionTable[i] = null;
						break;
					}
				}
			}
			if(i >= connection_size)
				System.out.println("error with close");
			close_server = true;
		}catch(Exception e){}
		System.out.println("Connection Closed");
	}
	
}

class ServerSocket {
	
	private static final int connection_size = 2;
	
	private BlockingQueue<DatagramPacket> acceptQueue;
	public BlockingQueue<DatagramPacket> outQueue;
	
	private DatagramSocket welcomeSocket;
	private Data[] connectionTable;
	private Thread omht, imht;
	private boolean acceptFlag, adding;
	private Counter connSocketCount;
	
	
	private class IMHT extends Thread {
		IMHT() {
		}

		public synchronized void run() {
			String data;
			
			// Get Incoming Msg
			while(true) {
				try {
					byte[] incomingData = new byte[1024];
					DatagramPacket incomingPacket = new DatagramPacket(incomingData,incomingData.length);
					welcomeSocket.receive(incomingPacket);
					
					DataPacket iPacket = toDataPacket(incomingPacket.getData());
					data = new String(iPacket.header);
					data = data.trim();
					int port = incomingPacket.getPort();
					InetAddress IPAddress = incomingPacket.getAddress();

					// If Msg = Conn Request
					if(data.equals("request")){
						
						// If Accept Queue Flag Raised, Lower Flag & Insert Conn Request in Accept Queue
						if(acceptFlag) {
							acceptFlag = false;
							int j;
							for(j = 0; j < connection_size; j++) {
								if(connectionTable[j] != null)
								{
									if(connectionTable[j].port == port && connectionTable[j].IP.equals(IPAddress))
									{
										break;
									}
								}
							}
							if (j >= connection_size)
							{
								acceptQueue.put(incomingPacket);
								System.out.println("IMHT to accept()");
							}
							else
								acceptFlag = true;
						}	
					}
					else { 
						int i;
						while(adding){}
						// Direct Msg to Correct Conn Socket via Shared Queue
						for(i = 0; i < connection_size; i++) {
							if(connectionTable[i] != null)
							{
								if(connectionTable[i].port == port && connectionTable[i].IP.equals(IPAddress))
								{
									connectionTable[i].socket.cimhtIn.put(incomingPacket);
									System.out.println("IMHT to CIMHT");
									break;
								}
							}
						}
                  
						if(i >= connection_size)
							System.out.println("No connection");
					}
					
				}catch(Exception e){
				}	
			}
		}
	}

	private class OMHT extends Thread {
		OMHT() {
		}

		public synchronized void run() {
			
			while(true)
			{
				try{
					welcomeSocket.send(outQueue.take());
					System.out.println("Message Sent");
					System.out.println("OMHT to Client");
				}catch(Exception e){}
			}
	}
	}
	
	public byte[] toByteArray (DataPacket packet)
	{
	  byte[] bytes = new byte[1024];
	  ByteArrayOutputStream bos = new ByteArrayOutputStream();
	  try {
	    ObjectOutputStream oos = new ObjectOutputStream(bos); 
	    oos.writeObject(packet);
	    oos.flush(); 
	    oos.close(); 
	    bos.close();
	    bytes = bos.toByteArray ();
	  }
	  catch (IOException ex) {}
	  
	  return bytes;
	}
	
	public DataPacket toDataPacket (byte[] bytes)
	{
	  DataPacket dp = new DataPacket();
	  try {
	    ByteArrayInputStream bis = new ByteArrayInputStream (bytes);
	    ObjectInputStream ois = new ObjectInputStream (bis);
	    dp = (DataPacket)ois.readObject();
	  }
	  catch (IOException ex) {}
	  catch (ClassNotFoundException ex) {}
	  
	  return dp;
	}
	
	// Create new welcome socket
	ServerSocket(int port) throws Exception {
		try{
			welcomeSocket = new DatagramSocket(port);
			acceptQueue = new ArrayBlockingQueue<DatagramPacket>(1);
			outQueue = new ArrayBlockingQueue<DatagramPacket>(2*connection_size);
			connSocketCount = new Counter(0);
			connectionTable = new Data[connection_size];
			
			omht = new OMHT();
			imht = new IMHT();
			
			omht.start();
			imht.start();
			
		}catch(Exception e){
			System.out.println("Port already in use");
		}
	}
	
	ConnectionSocket accept() throws Exception {
		ConnectionSocket socket = null;
		String ack = new String("accept");
		DataPacket ackPacket = new DataPacket(ack.getBytes(),null);
		try{
			acceptFlag = true;
			if(connSocketCount.getCount() < connection_size) {
				
				// Pull conn request out of the queue
				DatagramPacket packet = acceptQueue.take();
	
				// Insert into conn table
				InetAddress IPAddress = packet.getAddress();
				int port = packet.getPort();
				socket = new ConnectionSocket(outQueue,connectionTable,connSocketCount,port,IPAddress);

				Data connSocketData = new Data(IPAddress, port, socket);
	
				adding = true;
				for(int i = 0; i < connection_size; i++) {
					if(connectionTable[i] == null)
					{
							connectionTable[i] = connSocketData;
							connSocketCount.increment();
							adding = false;
							break;
					}
				}
				
				byte[] sendData = new byte[1024];
				sendData = toByteArray(ackPacket);
				packet = new DatagramPacket(sendData,sendData.length,IPAddress,port);
				welcomeSocket.send(packet);
				System.out.println("Connection Accepted");
			} 
		}catch(Exception e){}
		
		return socket;
	}

}




